package com.dman.multi.proto;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.dman.multi.proxy.ProxyAdaptor;
import com.dman.multi.proxy.ProxyDelegator;

public class FTPProtocolHandler implements ProtocolHandler {
	
	public static final String FTP_USER_CMD = "USER ";
	public static final int FTP_USER_CMD_STAT = 331;
	public static final String FTP_PASS_CMD = "PASS ";
	public static final int FTP_PASS_CMD_STAT = 230;
	public static final String FTP_BINARY_TYPE_CMD = "TYPE I";
	public static final int FTP_BINARY_TYPE_CMD_STAT = 200;
	public static final String FTP_PASV_CMD = "PASV";
	public static final int FTP_PASV_CMD_STAT = 227;
	public static final String FTP_PASV_CMD_REGEX = ".*?\\((\\d+,\\d+,\\d+,\\d+),(\\d+),(\\d+)\\)$";
	public static final String FTP_SIZE_CMD = "SIZE ";
	public static final int FTP_SIZE_CMD_STAT = 213;
	public static final String FTP_REST_CMD = "REST ";
	public static final int FTP_REST_CMD_STAT= 350;
	public static final String FTP_RETR_CMD = "RETR ";
	public static final int FTP_RETR_CMD_STAT = 150;
	//public static final int FTP_RETR_CMD_STAT2 = 226;
	public static final String FTP_CHARSET = "US-ASCII";
	public static final String FTP_LINE_TERMINATOR = "\r\n";
	public static final int BUFFER_CAPACITY = 1<<16;
	
	private SocketChannel ctrlChannel = null;
	private SocketChannel dataChannel = null;
	private ByteBuffer bb = ByteBuffer.allocate(BUFFER_CAPACITY);
	private ProxyDelegator pd = null;

	@Override
	public Long init(URL url, long offset, ProxyAdaptor proxyAuth) throws IOException, UnknownHostException {
		// create socket channels and the corresponding sockets
		ctrlChannel = SocketChannel.open();
		pd = ProxyDelegator.getInstance();
		pd.connect(ctrlChannel, proxyAuth, url);
		
		// read server response
		StringBuilder sb = new StringBuilder();
		if(!getStatus(ctrlChannel, 220, sb))
			throw new IOException("Unable to connect to server");
		
		dataChannel = SocketChannel.open();
		
		// try to obtain the user name and password from the URL
		String userInfo = url.getUserInfo();
		String userName = "anonymous";
		String password = "";
		if(userInfo != null){
			int indx = userInfo.indexOf(':');
			userName = userInfo.substring(0, indx);
			password = userInfo.substring(indx + 1);
		}
		
		// login with user name and password
		long size = 0;
		if(ctrlChannel.isConnected()){
			
			String cmd = FTP_USER_CMD + userName;
			write(ctrlChannel, cmd);
			if(!getStatus(ctrlChannel, FTP_USER_CMD_STAT, sb))
				throw new IOException("Error while specifying userName: " + userName);
			
			cmd = FTP_PASS_CMD + password;
			write(ctrlChannel, cmd);
			if(!getStatus(ctrlChannel, FTP_PASS_CMD_STAT, sb))
				throw new IOException("Error while specifying password: " + password);
			
			cmd = FTP_BINARY_TYPE_CMD;
			write(ctrlChannel, cmd);
			if(!getStatus(ctrlChannel, FTP_BINARY_TYPE_CMD_STAT, sb))
				throw new IOException("Error while specifying binary mode");
			
			cmd = FTP_PASV_CMD;
			write(ctrlChannel, cmd);
			if(!getStatus(ctrlChannel, FTP_PASV_CMD_STAT, sb))
				throw new IOException("Error while specifying passive mode");
			URL durl = getDataUrl(sb.toString());
			pd.connect(dataChannel, proxyAuth, durl);

			cmd = FTP_SIZE_CMD + " " + url.getPath();
			write(ctrlChannel, cmd);
			if(!getStatus(ctrlChannel, FTP_SIZE_CMD_STAT, sb))
				throw new IOException("Error while retrieving size");
			String strSize = sb.toString().split(" ") [0];
			size = Long.parseLong(strSize);

			cmd = FTP_REST_CMD + " " + offset;
			write(ctrlChannel, cmd);
			if(!getStatus(ctrlChannel, FTP_REST_CMD_STAT, sb)){
				System.out.println("REST FTP command not supported");
				size = -size;
			}

			cmd = FTP_RETR_CMD + " " + url.getPath();
			write(ctrlChannel, cmd);
			if(!getStatus(ctrlChannel, FTP_RETR_CMD_STAT, sb))
				throw new IOException("Error while specifying retr");
		}
		
		return size;
	}
	
	public void write(SocketChannel sockChannel, String cmd) throws IOException{
		cmd += FTP_LINE_TERMINATOR;
		sockChannel.write(ByteBuffer.wrap(cmd.getBytes(FTP_CHARSET)));
	}
	
	public boolean getStatus(SocketChannel sockChannel, int status, StringBuilder sb) throws IOException{
		Socket s = sockChannel.socket();
		InputStream is = s.getInputStream();
		String message = "";
		byte [] barr = new byte [1024];
		int len = 0;
		Pattern pattern = Pattern.compile("^([0-9]{3})\\s+(.*)");
		String resp  = "";
		read_iter:
		while((len = is.read(barr)) >= 0){
			message += new String(barr, 0, len, FTP_CHARSET);
			String [] sarr = message.split(FTP_LINE_TERMINATOR);
			for(String str: sarr){
				Matcher m = pattern.matcher(str);
				if(m.matches()){
					resp = m.group(1) + " " + m.group(2);
					break read_iter;
				}
			}
		}
		boolean correct = resp.startsWith(status + "");
		sb.delete(0, (sb.length() == 0)? 0:sb.length() - 1);
		sb.append(message);
		
		return correct;
	}
	
	public URL getDataUrl(String line) throws MalformedURLException{
		Pattern p = Pattern.compile(FTP_PASV_CMD_REGEX);
		Matcher m = p.matcher(line);
		URL url = null;
		if(m.matches()){
			String ip = m.group(1);
			ip = ip.replace(',', '.');
			String port1 = m.group(2);
			int port = Integer.parseInt(port1);
			port *= 256;
			String port2 = m.group(3);
			port += Integer.parseInt(port2);
			String hostPort = "ftp://" + ip + ":" + port;
			url = new URL(hostPort);
		}
		
		return url;
	}

	@Override
	public SocketChannel socketChannel() throws IOException {
		dataChannel.configureBlocking(false);
		return dataChannel;
	}

	@Override
	public void close() throws IOException {
		if(dataChannel != null)
			dataChannel.close();
		if(ctrlChannel != null)
			ctrlChannel.close();
		ctrlChannel = null;
		dataChannel = null;
	}

	@Override
	public ByteBuffer read() throws IOException {
		bb.clear();
		int rc = dataChannel.read(bb);
		if(rc < 0)
			throw new EOFException("End of file is reached");
		bb.flip();
		return bb;
	}

}
